4.2 参数

指针阅读好文:

Go 数据类型 指针详解:在什么情况下应该使用指针?

Go对参数的处理偏向保守,不支持有默认值的可选参数,不支持命名实参。调用时,必须按签名顺序传递指定类型和数量的实参,就算以“_”命名的参数也不能忽略。

在参数列表中,相邻的同类型参数可合并

func test(x,y int,s string, _bool) *int{ 
   return nil
} 
  
func main() { 
   test(1,2, "abc")     // 错误:not enough arguments in call to test
}
 

参数可视作函数局部变量,因此不能在相同层次定义同名变量。

func add(x,y int)int{ 
   x:=100            // 错误:no new variables on left side of:= 
   var y int           // 错误:y redeclared in this block
   return x+y
}

形参是指函数定义中的参数,实参则是函数调用时所传递的参数。形参类似函数局部变量,而实参则是函数外部对象,可以是常量、变量、表达式或函数等。

不管是指针、引用类型,还是其他类型参数,都是值拷贝传递(pass-by-value)。区别无非是拷贝目标对象,还是拷贝指针而已。在函数调用前,会为形参和返回值分配内存空间,并将实参拷贝到形参内存。

func test(x*int) { 
   fmt.Printf("pointer: %p,target: %v\n", &x,x)    // 输出形参x的地址 
} 
  
func main() { 
   a:=0x100
   p:= &a
   fmt.Printf("pointer: %p,target: %v\n", &p,p)    // 输出实参p的地址 
  
   test(p) 
}

输出:

pointer:0xc82002c020,target:0xc82000a298
pointer:0xc82002c030,target:0xc82000a298

从输出结果可以看出,尽管实参和形参都指向同一目标,但传递指针时依然被复制

表面上看,指针参数的性能要更好一些,但实际上得具体分析。被复制的指针会延长目标对象生命周期,还可能会导致它被分配到堆上,那么其性能消耗就得加上堆内存分配和垃圾回收的成本。

其实在栈上复制小对象只须很少的指令即可完成,远比运行时进行堆内存分配要快得多。另外,并发编程也提倡尽可能使用不可变对象(只读或复制),这可消除数据同步等麻烦。当然,如果复制成本很高,或需要修改原对象状态,自然使用指针更好

下面是一个指针参数导致实参变量被分配到堆上的简单示例。可对比传值参数的汇编代码,从中可看出具体的差别。

func test(p*int) { 
   go func() {              // 延长p生命周期 
       println(p) 
    }() 
} 
  
func main() { 
   x:=100
   p:= &x
   test(p) 
}
 

输出:

$go build-gcflags"-m"         // 输出编译器优化策略 
  
moved to heap:x
&x escapes to heap             // 逃逸 
  
  
$go tool objdump-s"main\.main"test
  
TEXT main.main(SB)test.go
   CALL runtime.newobject(SB)         // 在堆上为x分配内存 
   CALL main.test(SB)

要实现传出参数(out),通常建议使用返回值。当然,也可继续用二级指针。

func test(p**int) { 
   x:=100
    *p= &x
} 
  
func main() { 
   var p*int
   test(&p) 
   println(*p) 
}
 

输出:

100

如果函数参数过多,建议将其重构为一个复合结构类型,也算是变相实现可选参数和命名实参功能。

type serverOption struct{ 
   address string
   port  int
   path  string
   timeout time.Duration
   log    *log.Logger
} 
  
func newOption() *serverOption{ 
   return &serverOption{             // 默认参数 
       address: "0.0.0.0", 
       port:   8080, 
       path:    "/var/test", 
       timeout:time.Second*5, 
       log:    nil, 
    } 
} 
  
func server(option *serverOption) {} 
  
func main() { 
   opt:=newOption() 
   opt.port=8085         // 命名参数设置 
  
   server(opt) 
}

将过多的参数独立成option struct,既便于扩展参数集,也方便通过newOption函数设置默认配置。这也是代码复用的一种方式,避免多处调用时烦琐的参数配置。

变参

变参本质上就是一个切片。只能接收一到多个同类型参数,且必须放在列表尾部。

func test(s string,a...int) { 
   fmt.Printf("%T, %v\n",a,a)       // 显示类型和值 
} 
  
func main() { 
   test("abc",1,2,3,4) 
}

输出:

[]int, [1 2 3 4]

将切片作为变参时,须进行展开操作。如果是数组,先将其转换为切片。

func test(a...int) { 
   fmt.Println(a) 
} 
  
func main() { 
   a:= [3]int{10,20,30} 
   test(a[:]...)            // 转换为slice后展开 
}

既然变参是切片,那么参数复制的仅是切片自身,并不包括底层数组,也因此可修改原数据。如果需要,可用内置函数copy复制底层数据。

func test(a...int) { 
   for i:=range a{ 
       a[i] +=100
    } 
} 
  
func main() { 
   a:= []int{10,20,30} 
   test(a...) 
  
   fmt.Println(a) 
}
 

输出:

[110 120 130]